﻿
namespace Patterns4Net.ArchitectureExplorer.Diagram
{
    using System;
    using System.Diagnostics.Contracts;
    using Attributes.Relations;

    /// <summary>
    /// Represents a relation in the diagram. 
    /// </summary>
    /// <remarks>
    /// <para>
    /// Relations can be bidirectional, or unidirectional. 
    /// The <see cref="IsBidirectional"/> property indicates whether a relation is bidirectional.
    /// Relations can have cardinality, bidirectional relations have cardinalities in both directions.
    /// </para>
    /// </remarks>
    public class Relation : ICloneable
    {
        private Cardinality sourceCardinality;

        /// <summary>
        /// Initializes a new instance of the <see cref="Relation"/> class as 
        /// an unidirectional relation. To create bidirectional relation use the 
        /// other constructor.
        /// </summary>
        /// <param name="source">The source element.</param>
        /// <param name="target">The target element.</param>
        /// <param name="cardinality">The cardinality or relation from source to target.</param>
        /// <param name="name">The name of the relation.</param>
        public Relation(
            DiagramElement source, 
            DiagramElement target, 
            Cardinality cardinality, 
            string name = "")
        {
            this.Source = source;
            this.Target = target;
            this.TargetCardinality = cardinality;
            this.IsBidirectional = false;
            this.HasCardinalities = true;
            this.Name = name;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Relation"/> class as an bidirectional 
        /// relation. To create just unidirectional relation use the other constructor.
        /// </summary>
        /// <param name="source">The source element.</param>
        /// <param name="target">The target element.</param>
        /// <param name="targetCardinality">The cardinality of relation from source to target.</param>
        /// <param name="sourceCardinality">The cardinality of relation from target to source.</param>
        /// <param name="name">The name of the relation.</param>
        public Relation(
            DiagramElement source,
            DiagramElement target,
            Cardinality targetCardinality,
            Cardinality sourceCardinality,
            string name = "")
        {
            this.Source = source;
            this.Target = target;
            this.TargetCardinality = targetCardinality;
            this.sourceCardinality = sourceCardinality;
            this.IsBidirectional = true;
            this.HasCardinalities = true;
            this.Name = name;            
        }

        public string Name { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether cardinalities 
        /// should be shown, e.g., in <see cref="TargetCardinalityString"/>..
        /// </summary>
        public bool HasCardinalities { get; set; }

        public DiagramElement Source { get; private set; }

        public DiagramElement Target { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is bidirectional. 
        /// When the relation is bidirectional, then <see cref="SourceCardinality"/> 
        /// is valid to access.
        /// </summary>
        public bool IsBidirectional { get; private set; }


        /// <summary>
        /// Gets or sets the cardinality of the relation from the <see cref="Source"/> to <see cref="Target"/>.
        /// </summary>
        public Cardinality TargetCardinality { get; private set; }

        /// <summary>
        /// Gets or sets the cardinality of the relation from the <see cref="Target"/> to <see cref="Source"/>.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This value is not available when the relation is not bidirectional, 
        /// and accessing it in such case will lead to <see cref="InvalidOperationException"/>.
        /// </para>
        /// </remarks>
        public Cardinality SourceCardinality
        {
            get
            {
                if (this.IsBidirectional == false)
                {
                    throw new InvalidOperationException(
                        string.Format(
                        "SourceCardinality is not available for non-bidirectional relations. Error occurred in relation {0}." +
                        "Check IsBidirectional property before accessing TargetCardinality property.",
                        this));
                }

                return this.sourceCardinality;
            }
        }

        /// <summary>
        /// Gets the string representing the <see cref="TargetCardinality"/>.         
        /// </summary>
        /// <remarks>
        /// <para>
        /// Cardinality strings may be different for example if in 
        /// both direction the cardinality is <see cref="Cardinality.Multiple"/>.
        /// </para>
        /// </remarks>
        public virtual string TargetCardinalityString
        {
            get
            {
                if (this.HasCardinalities)
                {
                    return GetCardinalityString(this.TargetCardinality);
                }
                else
                {
                    return string.Empty;
                }
            }
        }

        /// <summary>
        /// Gets the string representing the <see cref="SourceCardinality"/>.   
        /// </summary>
        /// <remarks>
        /// <para>
        /// This value is not available when the relation is not bidirectional, 
        /// and accessing it in such case will lead to <see cref="InvalidOperationException"/>.
        /// </para>
        /// </remarks>
        /// <seealso cref="TargetCardinalityString"/>
        public virtual string SourceCardinalityString
        {
            get
            {
                if (this.HasCardinalities == false)
                {
                    return string.Empty;
                }

                if (this.IsBidirectional && 
                    this.SourceCardinality == Cardinality.Multiple &&
                    this.TargetCardinality == Cardinality.Multiple)
                {
                    return "M";
                }

                return GetCardinalityString(this.SourceCardinality);
            }
        }

        public virtual object TargetArrow
        {
            get { return new RelationArrow(); }
        }

        public virtual object SourceArrow
        {
            get { return new RelationArrow(); }
        }

        public virtual bool CanBeMerged
        {
            get { return this.IsBidirectional == false; }
        }

        /// <summary>
        /// Creates a relation of the same type. This is used for making relations clones. 
        /// All other parameters of relation other than source and target should be cloned.
        /// </summary>
        public virtual Relation CreateBidirectional(
            DiagramElement source, 
            DiagramElement target,
            Cardinality targetCardinality,
            Cardinality sourceCardinality)
        {
            var result = (Relation)this.Clone();
            result.Source = source;
            result.Target = target;
            result.sourceCardinality = sourceCardinality;
            result.TargetCardinality = targetCardinality;
            result.IsBidirectional = true;
            return result;
        }

        /// <summary>
        /// Creates the same relation, but between different elements.
        /// This is used to create relation between packages or layers according 
        /// to existing relation between types they contain.
        /// </summary>
        public virtual Relation Create(DiagramElement source, DiagramElement target)
        {
            var result = (Relation)this.Clone();
            result.Source = source;
            result.Target = target;
            return result;
        }

        /// <summary>
        /// Returns <c>true</c> if this object represents a relation from source to target.
        /// This method takes into account that a <see cref="Relation"/> object may represent 
        /// a bidirectional relation. 
        /// </summary>
        public bool Equals(DiagramElement source, DiagramElement target)
        {
            return (this.Source == source && this.Target == target) ||
                   (this.IsBidirectional && this.Target == source && this.Source == target);
        }

        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
            {
                return false;
            }

            var relation = (Relation)obj;
            return this.Source.Equals(relation.Source) &&
                this.Target.Equals(relation.Target);            
        }

        public override int GetHashCode()
        {
            return this.Source.GetHashCode() ^ this.Target.GetHashCode() ^ this.GetType().GetHashCode();
        }

        public virtual object Clone()
        {
            return this.MemberwiseClone();
        }

        protected static string GetCardinalityString(Cardinality cardinality)
        {
            switch (cardinality)
            {
                case Cardinality.Multiple:
                    return "N";
                case Cardinality.One:
                    return "1";
                case Cardinality.None:
                    return string.Empty;
                default:
                    Contract.Assert(false, "Unexpected value of Cardinality enumeration in switch.");
                    return string.Empty;
            }
        }

        public override string ToString()
        {
            if (this.IsBidirectional)
            {
                return string.Format(
                    "{0} ({1}) <-> ({2}) {3}",
                    this.Source,
                    this.SourceCardinalityString,
                    this.TargetCardinalityString,
                    this.Target);                
            }

            return string.Format(
                "{0} -> ({1}) {2}",
                this.Source,
                this.TargetCardinalityString,
                this.Target);                
        }
    }

    public class RelationArrow
    {        
    }
}
